Table of Contents

  • 1  Загрузите данные и подготовьте их к анализу
    • 1.1  Предобработка данных
      • 1.1.1  Таблица с визитами
      • 1.1.2  Таблица с заказами
      • 1.1.3  Таблица с расходами
  • 2  Задайте функции для расчёта и анализа LTV, ROI, удержания и конверсии.
    • 2.1  Зададим функции для вычисления метрик
    • 2.2  Зададим функции для построения графиков
  • 3  Исследовательский анализ данных
    • 3.1  Создание профилей пользователей
    • 3.2  Предпочтения пользователей
  • 4  Маркетинг
    • 4.1  Расходы на маркетинг
    • 4.2  Динамика расходов
    • 4.3  Стоимость привлечения одного пользователя
  • 5  Оцените окупаемость рекламы
    • 5.1  Подготовка данных
    • 5.2  Окупаемость рекламы
    • 5.3  Конверсия покупателей и её динамика
    • 5.4  Удержание пользователей
    • 5.5  Окупаемость рекламы с разбивкой по устройствам
      • 5.5.1  Конверсия пользователей в зависимости от устройства
      • 5.5.2  Удержание пользователей в зависимости от устройства
    • 5.6  Окупаемость рекламы с разбивкой на регионы
      • 5.6.1  Конверсия пользователей в зависимости от региона
      • 5.6.2  Удержание пользователей в зависимости от региона
    • 5.7  Окупаемость рекламы с разбивкой на каналы привлечения
      • 5.7.1  Конверсия пользователей в зависимости от канала привлечения
      • 5.7.2  Удержание пользователей в зависимости от канала привлечения
      • 5.7.3  Успешность рекламных акций в разных странах
        • 5.7.3.1  Для США
        • 5.7.3.2  Для Европы
    • 5.8  Вывод
  • 6  Напишите выводы

Описание¶

В данной работе мы проводим анализ бизнес-показателей для развлекательного приложения Procrastinate Pro+. Цель исследования - Помочь компании минимизировать убытки и выйти в плюс.

Нашей задачей будет рассмотреть, почему компания терпит убытки. Для этого мы изучим следующее:

  • откуда приходят пользователи и какими устройствами они пользуются,
  • сколько стоит привлечение пользователей из различных рекламных каналов;
  • сколько денег приносит каждый клиент,
  • когда расходы на привлечение клиента окупаются,
  • какие факторы мешают привлечению клиентов.

Для получения этой информации воспользуемся уже известными функциями для изучения метрик.

Подготовка к работе¶

Загрузим все необходимые нам библиотеки

In [1]:
import pandas as pd
from datetime import datetime, timedelta
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

Загрузите данные и подготовьте их к анализу¶

К содержанию

Загрузите данные о визитах, заказах и рекламных расходах из CSV-файлов в переменные.

Пути к файлам

  • визиты: /datasets/visits_info_short.csv. Скачать датасет;
  • заказы: /datasets/orders_info_short.csv. Скачать датасет;
  • расходы: /datasets/costs_info_short.csv. Скачать датасет.

Изучите данные и выполните предобработку. Есть ли в данных пропуски и дубликаты? Убедитесь, что типы данных во всех колонках соответствуют сохранённым в них значениям. Обратите внимание на столбцы с датой и временем.

Предобработка данных¶

К содержанию

Для начала, загрузим всю необходимую для анализа информацию

In [2]:
path = 'D:\\Irina\\datasets\\'
visits = pd.read_csv(path + 'visits_info_short.csv')
orders = pd.read_csv(path + 'orders_info_short.csv')
costs = pd.read_csv(path + 'costs_info_short.csv')
Таблица с визитами¶

К содержанию

In [3]:
display(visits.head())
visits.info()
User Id Region Device Channel Session Start Session End
0 981449118918 United States iPhone organic 2019-05-01 02:36:01 2019-05-01 02:45:01
1 278965908054 United States iPhone organic 2019-05-01 04:46:31 2019-05-01 04:47:35
2 590706206550 United States Mac organic 2019-05-01 14:09:25 2019-05-01 15:32:08
3 326433527971 United States Android TipTop 2019-05-01 00:29:59 2019-05-01 00:54:25
4 349773784594 United States Mac organic 2019-05-01 03:33:35 2019-05-01 03:57:40
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 309901 entries, 0 to 309900
Data columns (total 6 columns):
 #   Column         Non-Null Count   Dtype 
---  ------         --------------   ----- 
 0   User Id        309901 non-null  int64 
 1   Region         309901 non-null  object
 2   Device         309901 non-null  object
 3   Channel        309901 non-null  object
 4   Session Start  309901 non-null  object
 5   Session End    309901 non-null  object
dtypes: int64(1), object(5)
memory usage: 14.2+ MB

В таблице с визитами представлены следующие колонки:

  • user_id - хранит уникальные коды пользователей;
  • region - страны, из которых пользователи посещали приложение;
  • device - устройство, с которого пользователи заходили в приложение;
  • channel - канал, через который пользователь "пришел" в приложение;
  • session start - дата и время начала сессии;
  • session end - дата и время конца сессии.

Что можно сказать о таблице:

  1. Стоит привести названия столбов к нижнему регистру, а также к "snake case" для удобства работы;
  2. Последние две колонки стоит привести к типу даты, чтобы иметь возможность работать с датами;
  3. Идентификаторы пользователей можно привести к типу object, чтобы случайно не получить неожиданные результаты, но в данном случае этим можно пренебречь;
  4. На первый взгляд в таблице нет пропусков, позже уточним этот пункт.

Приступим к обработке:

In [4]:
#Переименуем столбцы, чтобы было удобно работать, можно было привести их к нижнему регистру,
#однако, нам нужно также привести названия к snake case
visits.columns = ['user_id','region','device','channel','session_start','session_end']
In [5]:
#Меняем типы данных
visits['session_start'] = pd.to_datetime(visits['session_start'])
visits['session_end'] = pd.to_datetime(visits['session_end'])

Проверим, совпадают ли даты в таблице с датами, указанными в описании прокета - 1 мая 2019 года - 27 октября 2013 года:

In [6]:
print('Первая дата посещения:', visits['session_start'].min(),
      '\nПоследняя дата посещения:', visits['session_start'].max())
Первая дата посещения: 2019-05-01 00:00:41 
Последняя дата посещения: 2019-10-31 23:59:23

Что же, начало сессий пользователей совпадает с теоритическими данными, а вот последняя дата начала сессии несколько выбивается. Но, в таблице скорее всего находятся данные повторных посещений пользователей, а это значит, что проблемы в данных нет. Но это мы узнаем позже, когда составим профили наших пользователей.

Перейдём к изучению пропусков и дубликатов

In [7]:
#Проверяем наличие пропусков в каждой колонке
visits.isna().sum()
Out[7]:
user_id          0
region           0
device           0
channel          0
session_start    0
session_end      0
dtype: int64
In [8]:
#Проверим наличие явных дубликатов
print('Количество явных дубликатов в таблице:', visits.duplicated().sum())
Количество явных дубликатов в таблице: 0
In [9]:
#Проверяем наличие неявных дубликатов
print('Уникальные регионы:', visits['region'].unique())
print('Уникальные типы устройств пользователей:', visits['device'].unique())
print('Уникальные каналы привлечения:', visits['channel'].unique())
Уникальные регионы: ['United States' 'UK' 'France' 'Germany']
Уникальные типы устройств пользователей: ['iPhone' 'Mac' 'Android' 'PC']
Уникальные каналы привлечения: ['organic' 'TipTop' 'RocketSuperAds' 'YRabbit' 'FaceBoom' 'MediaTornado'
 'AdNonSense' 'LeapBob' 'WahooNetBanner' 'OppleCreativeMedia'
 'lambdaMediaAds']

В таблице отсутсвтуют пропуски и дубликаты, с этой таблицей уже можно спокойно работать. Приступим к следующей таблице.

Таблица с заказами¶

К содержанию

Выведем таблицу с заказами и основную информацию о ней

In [10]:
display(orders.head())
display(orders.info())
#Проверим статистические показатели числовых данных
orders['Revenue'].describe()
User Id Event Dt Revenue
0 188246423999 2019-05-01 23:09:52 4.99
1 174361394180 2019-05-01 12:24:04 4.99
2 529610067795 2019-05-01 11:34:04 4.99
3 319939546352 2019-05-01 15:34:40 4.99
4 366000285810 2019-05-01 13:59:51 4.99
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40212 entries, 0 to 40211
Data columns (total 3 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   User Id   40212 non-null  int64  
 1   Event Dt  40212 non-null  object 
 2   Revenue   40212 non-null  float64
dtypes: float64(1), int64(1), object(1)
memory usage: 942.6+ KB
None
Out[10]:
count    40212.000000
mean         5.370608
std          3.454208
min          4.990000
25%          4.990000
50%          4.990000
75%          4.990000
max         49.990000
Name: Revenue, dtype: float64

Здесь есть только три колонки:

  1. user id - уникальный идентификатор пользователя, по которому мы сможем объединить таблицы для получения необходимой информации
  2. event dt - дата и время, в которое произошло событие (покупка)
  3. revenue - сумма покупки

Как и в предыдущем случае, следует привести названия колонок к единому и удобному стилю, поменять тип в колонке, содержащей дату и проверить качество данных. В столбце, содержащем сумму покупки, не отмечается аномальных данных.

In [11]:
#Приводим названия колонок к "хорошему" стилю
orders.columns = ['user_id','event_dt','revenue']
In [12]:
#Изменяем тип данных колонки, чтобы можно было работать с датами в ней
orders['event_dt'] = pd.to_datetime(orders['event_dt'])
In [13]:
#Проверим даты и здесь
print('Первая дата покупки:', orders['event_dt'].min(),
      '\nПоследняя дата покупки:', orders['event_dt'].max())
Первая дата покупки: 2019-05-01 00:28:11 
Последняя дата покупки: 2019-10-31 23:56:56

Здесь также первая покупка совпадает с теоритическими данными, а вот последняя дата выбивается, но это не значит, что в данные закралась ошибка. Посетители могли совершить покупку позднее привлечения. Продолжим предобработку.

In [14]:
#Проверяем наличие пропусков
orders.isna().sum()
Out[14]:
user_id     0
event_dt    0
revenue     0
dtype: int64
In [15]:
print('Количество явных дубликатов в таблице:', orders.duplicated().sum())
Количество явных дубликатов в таблице: 0

В данном случае мы не можем проверить неявные дубликаты, потому что здесь нет текстовых данных, в которых могут быть ошибки. Поэтому, с обработкой данной таблицы мы закончили и пришло время приступить к следующей таблице.

Таблица с расходами¶

К содержанию

Теперь перейдём к таблице с расходами на рекламу и получим основную информацию о ней.

In [16]:
display(costs.head())
display(costs.info())
costs['costs'].describe()
dt Channel costs
0 2019-05-01 FaceBoom 113.3
1 2019-05-02 FaceBoom 78.1
2 2019-05-03 FaceBoom 85.8
3 2019-05-04 FaceBoom 136.4
4 2019-05-05 FaceBoom 122.1
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1800 entries, 0 to 1799
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype  
---  ------   --------------  -----  
 0   dt       1800 non-null   object 
 1   Channel  1800 non-null   object 
 2   costs    1800 non-null   float64
dtypes: float64(1), object(2)
memory usage: 42.3+ KB
None
Out[16]:
count    1800.000000
mean       58.609611
std       107.740223
min         0.800000
25%         6.495000
50%        12.285000
75%        33.600000
max       630.000000
Name: costs, dtype: float64

Таблица содержит три колонки:

  1. dt - дата проведения рекламной кампании
  2. channel - рекламный источник
  3. costs - сколько стоила кампания в этот день

В стоимости рекламы пока всё выглядит нормально, за исключением значения в 630 долларов. Либо в один из дней фирма решила провести масштабную рекламную акцию, либо это какая-то ошибка. К сожалению, мы не можем предположить что могло быть причиной такого и действительно ли можно считать это аномалией. Проведем преобразования и исследование данных по аналогии с предыдущими таблицами.

In [17]:
# В данном случае не нужно приводить к snake case
costs.columns = costs.columns.str.lower()
In [18]:
#Меняем тип данных и получаем дату
costs['dt'] = pd.to_datetime(costs['dt']).dt.date
In [19]:
#Проверим даты привлечения по проведению рекламных кампаний
print('Первая дата проведения кампании:', costs['dt'].min(),
      '\nПоследняя дата проведения кампании:', costs['dt'].max())
Первая дата проведения кампании: 2019-05-01 
Последняя дата проведения кампании: 2019-10-27

Здесь никаких аномалий нет, привлечение клиентов проводилось с 1 мая по 27 октября 2019 года, как и указано в описании данных.

In [20]:
#Смотрим пропуски
costs.isna().sum()
Out[20]:
dt         0
channel    0
costs      0
dtype: int64
In [21]:
print('Количество явных дубликатов в таблице:', costs.duplicated().sum())
Количество явных дубликатов в таблице: 0
In [22]:
#Смотрим возможные опечатки для определения неявных дубликатов
print('Уникальные каналы привлечения:', costs['channel'].unique())
Уникальные каналы привлечения: ['FaceBoom' 'MediaTornado' 'RocketSuperAds' 'TipTop' 'YRabbit'
 'AdNonSense' 'LeapBob' 'OppleCreativeMedia' 'WahooNetBanner'
 'lambdaMediaAds']

Обработку данных мы завершили, теперь с тремя таблицами можно работать. Пора приступить к заданию необходимых для исследования инструментов.

Задайте функции для расчёта и анализа LTV, ROI, удержания и конверсии.¶

К содержанию

Разрешается использовать функции, с которыми вы познакомились в теоретических уроках.

Это функции для вычисления значений метрик:

  • get_profiles() — для создания профилей пользователей,
  • get_retention() — для подсчёта Retention Rate,
  • get_conversion() — для подсчёта конверсии,
  • get_ltv() — для подсчёта LTV.

А также функции для построения графиков:

  • filter_data() — для сглаживания данных,
  • plot_retention() — для построения графика Retention Rate,
  • plot_conversion() — для построения графика конверсии,
  • plot_ltv_roi — для визуализации LTV и ROI.

Зададим функции для вычисления метрик¶

К содержанию

Функция, позволяющая объединить исходные таблицы и получить полные данные каждого пользователя. Также добавляет признаки пользователей (покупатели/ не покупатели) и добавляет стоимость привлечения пользователя.

In [23]:
def get_profiles(visits, orders, costs):

    # находим параметры первых посещений
    profiles = (
        visits.sort_values(by=['user_id', 'session_start'])
        .groupby('user_id')
        .agg(
            {
                'session_start': 'first',
                'channel': 'first',
                'device': 'first',
                'region': 'first',
            }
        )
        .rename(columns={'session_start': 'first_ts'})
        .reset_index()
    )

    # Определяем дату первого посещения и первый день месяца в который это произошло
    profiles['dt'] = profiles['first_ts'].dt.date
    profiles['month'] = profiles['first_ts'].astype('datetime64[M]')

    # добавляем признак платящих пользователей
    profiles['payer'] = profiles['user_id'].isin(orders['user_id'].unique())

    # считаем количество уникальных пользователей с одинаковыми источником и датой привлечения
    new_users = (
        profiles.groupby(['dt', 'channel'])
        .agg({'user_id': 'nunique'})
        .rename(columns={'user_id': 'unique_users'})
        .reset_index()
    )

    # объединяем траты на рекламу и число привлечённых пользователей
    costs = costs.merge(new_users, on=['dt', 'channel'], how='left')

    # делим рекламные расходы на число привлечённых пользователей
    costs['acquisition_cost'] = costs['costs'] / costs['unique_users']
 

    # добавляем стоимость привлечения в профили
    profiles = profiles.merge(
        costs[['dt', 'channel', 'acquisition_cost']],
        on=['dt', 'channel'],
        how='left',
    )

    # стоимость привлечения органических пользователей равна нулю
    profiles['acquisition_cost'] = profiles['acquisition_cost'].fillna(0)

    return profiles

Функция для построения таблицы удержания и динамики удержания

In [24]:
def get_retention(
    profiles,
    sessions,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # добавляем столбец payer в передаваемый dimensions список
    dimensions = ['payer'] + dimensions

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # собираем «сырые» данные для расчёта удержания
    result_raw = result_raw.merge(
        sessions[['user_id', 'session_start']], on='user_id', how='left'
    )
    result_raw['lifetime'] = (
        result_raw['session_start'] - result_raw['first_ts']
    ).dt.days

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу удержания
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    # получаем таблицу динамики удержания
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time 

Функция для построения таблицы конверсии и динамики конверсии

In [25]:
def get_conversion(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # определяем дату и время первой покупки для каждого пользователя
    first_purchases = (
        purchases.sort_values(by=['user_id', 'event_dt'])
        .groupby('user_id')
        .agg({'event_dt': 'first'})
        .reset_index()
    )

    # собираем «сырые» данные для расчёта удержания
    result_raw = result_raw.merge(
        first_purchases[['user_id', 'event_dt']], on='user_id', how='left'
    )

    
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days

    
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users' 
        dimensions = dimensions + ['cohort']

    # функция для группировки таблицы по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        result = df.pivot_table(
            index=dims, columns='lifetime', values='user_id', aggfunc='nunique'
        )
        result = result.fillna(0).cumsum(axis = 1)
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # делим каждую «ячейку» в строке на размер когорты
        # и получаем conversion rate
        result = result.div(result['cohort_size'], axis=0)
        result = result[['cohort_size'] + list(range(horizon_days))]
        result['cohort_size'] = cohort_sizes
        return result

    # получаем таблицу конверсии
    result_grouped = group_by_dimensions(result_raw, dimensions, horizon_days)

    
    if 'cohort' in dimensions: 
        dimensions = []

    # получаем таблицу динамики конверсии
    result_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    # возвращаем обе таблицы и сырые данные
    return result_raw, result_grouped, result_in_time 

Функция для построения таблиц LTV, ROI, их динамики, а также для рассчета CAC

In [26]:
def get_ltv(
    profiles,
    purchases,
    observation_date,
    horizon_days,
    dimensions=[],
    ignore_horizon=False,
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')
    # добавляем данные о покупках в профили
    result_raw = result_raw.merge(
        purchases[['user_id', 'event_dt', 'revenue']], on='user_id', how='left'
    )
    # рассчитываем лайфтайм пользователя для каждой покупки
    result_raw['lifetime'] = (
        result_raw['event_dt'] - result_raw['first_ts']
    ).dt.days
    # группируем по cohort, если в dimensions ничего нет
    if len(dimensions) == 0:
        result_raw['cohort'] = 'All users'
        dimensions = dimensions + ['cohort']

    # функция группировки по желаемым признакам
    def group_by_dimensions(df, dims, horizon_days):
        # строим «треугольную» таблицу выручки
        result = df.pivot_table(
            index=dims, columns='lifetime', values='revenue', aggfunc='sum'
        )
        # находим сумму выручки с накоплением
        result = result.fillna(0).cumsum(axis=1)
        # вычисляем размеры когорт
        cohort_sizes = (
            df.groupby(dims)
            .agg({'user_id': 'nunique'})
            .rename(columns={'user_id': 'cohort_size'})
        )
        # объединяем размеры когорт и таблицу выручки
        result = cohort_sizes.merge(result, on=dims, how='left').fillna(0)
        # считаем LTV: делим каждую «ячейку» в строке на размер когорты
        result = result.div(result['cohort_size'], axis=0)
        # исключаем все лайфтаймы, превышающие горизонт анализа
        result = result[['cohort_size'] + list(range(horizon_days))]
        # восстанавливаем размеры когорт
        result['cohort_size'] = cohort_sizes

        # собираем датафрейм с данными пользователей и значениями CAC, 
        # добавляя параметры из dimensions
        cac = df[['user_id', 'acquisition_cost'] + dims].drop_duplicates()

        # считаем средний CAC по параметрам из dimensions
        cac = (
            cac.groupby(dims)
            .agg({'acquisition_cost': 'mean'})
            .rename(columns={'acquisition_cost': 'cac'})
        )

        # считаем ROI: делим LTV на CAC
        roi = result.div(cac['cac'], axis=0)

        # удаляем строки с бесконечным ROI
        roi = roi[~roi['cohort_size'].isin([np.inf])]

        # восстанавливаем размеры когорт в таблице ROI
        roi['cohort_size'] = cohort_sizes

        # добавляем CAC в таблицу ROI
        roi['cac'] = cac['cac']

        # в финальной таблице оставляем размеры когорт, CAC
        # и ROI в лайфтаймы, не превышающие горизонт анализа
        roi = roi[['cohort_size', 'cac'] + list(range(horizon_days))]

        # возвращаем таблицы LTV и ROI
        return result, roi

    # получаем таблицы LTV и ROI
    result_grouped, roi_grouped = group_by_dimensions(
        result_raw, dimensions, horizon_days
    )

    # для таблиц динамики убираем 'cohort' из dimensions
    if 'cohort' in dimensions:
        dimensions = []

    # получаем таблицы динамики LTV и ROI
    result_in_time, roi_in_time = group_by_dimensions(
        result_raw, dimensions + ['dt'], horizon_days
    )

    return (
        result_raw,  # сырые данные
        result_grouped,  # таблица LTV
        result_in_time,  # таблица динамики LTV
        roi_grouped,  # таблица ROI
        roi_in_time,  # таблица динамики ROI
    ) 

Зададим функции для построения графиков¶

К содержанию

Функция для "сглаживания" данных, чтобы строить более читаемые графики

In [27]:
def filter_data(df, window):
    # для каждого столбца применяем скользящее среднее
    for column in df.columns.values:
        df[column] = df[column].rolling(window).mean() 
    return df

Функция для построения графиков удержания и динамики удержания:

In [28]:
def plot_retention(retention, retention_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 10))

    # исключаем размеры когорт и удержание первого дня
    retention = retention.drop(columns=['cohort_size', 0])
    # в таблице динамики оставляем только нужный лайфтайм
    retention_history = retention_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # если в индексах таблицы удержания только payer,
    # добавляем второй признак — cohort
    if retention.index.nlevels == 1:
        retention['cohort'] = 'All users'
        retention = retention.reset_index().set_index(['cohort', 'payer'])

    # в таблице графиков — два столбца и две строки, четыре ячейки
    # в первой строим кривые удержания платящих пользователей
    ax1 = plt.subplot(2, 2, 1)
    retention.query('payer == True').droplevel('payer').T.plot(
        grid=True, ax=ax1
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.ylabel('Доля удержания')
    plt.title('Удержание платящих пользователей')

    # во второй ячейке строим кривые удержания неплатящих
    # вертикальная ось — от графика из первой ячейки
    ax2 = plt.subplot(2, 2, 2, sharey=ax1)
    retention.query('payer == False').droplevel('payer').T.plot(
        grid=True, ax=ax2
    )
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.ylabel('Доля удержания')
    plt.title('Удержание неплатящих пользователей')

    # в третьей ячейке — динамика удержания платящих
    ax3 = plt.subplot(2, 2, 3)
    # получаем названия столбцов для сводной таблицы
    columns = [
        name
        for name in retention_history.index.names
        if name not in ['dt', 'payer']
    ]
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == True').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.ylabel('Доля удержания')
    plt.title(
        'Динамика удержания платящих пользователей на {}-й день'.format(
            horizon
        )
    )

    # в чётвертой ячейке — динамика удержания неплатящих
    ax4 = plt.subplot(2, 2, 4, sharey=ax3)
    # фильтруем данные и строим график
    filtered_data = retention_history.query('payer == False').pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax4)
    plt.xlabel('Дата привлечения')
    plt.ylabel('Доля удержания')
    plt.title(
        'Динамика удержания неплатящих пользователей на {}-й день'.format(
            horizon
        )
    )
    
    plt.tight_layout()
    plt.show() 

Функция для построения графиков конверсии и динамики конверсии:

In [29]:
def plot_conversion(conversion, conversion_history, horizon, window=7):

    # задаём размер сетки для графиков
    plt.figure(figsize=(15, 5))

    # исключаем размеры когорт
    conversion = conversion.drop(columns=['cohort_size'])
    # в таблице динамики оставляем только нужный лайфтайм
    conversion_history = conversion_history.drop(columns=['cohort_size'])[
        [horizon - 1]
    ]

    # первый график — кривые конверсии
    ax1 = plt.subplot(1, 2, 1)
    conversion.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.ylabel('Доля конверсии')
    plt.title('Конверсия пользователей')

    # второй график — динамика конверсии
    ax2 = plt.subplot(1, 2, 2, sharey=ax1)
    columns = [
        # столбцами сводной таблицы станут все столбцы индекса, кроме даты
        name for name in conversion_history.index.names if name not in ['dt']
    ]
    filtered_data = conversion_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.ylabel('Доля конверсии')
    plt.title('Динамика конверсии пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()

Функция для построения графиков LTV, ROI и их динамик, а также CAC. Для того, чтобы оценить окупаемость рекламы:

In [30]:
def plot_ltv_roi(ltv, ltv_history, roi, roi_history, horizon, window=7):

    # задаём сетку отрисовки графиков
    plt.figure(figsize=(20, 10))

    # из таблицы ltv исключаем размеры когорт
    ltv = ltv.drop(columns=['cohort_size'])
    # в таблице динамики ltv оставляем только нужный лайфтайм
    ltv_history = ltv_history.drop(columns=['cohort_size'])[[horizon - 1]]

    # стоимость привлечения запишем в отдельный фрейм
    cac_history = roi_history[['cac']]

    # из таблицы roi исключаем размеры когорт и cac
    roi = roi.drop(columns=['cohort_size', 'cac'])
    # в таблице динамики roi оставляем только нужный лайфтайм
    roi_history = roi_history.drop(columns=['cohort_size', 'cac'])[
        [horizon - 1]
    ]

    # первый график — кривые ltv
    ax1 = plt.subplot(2, 3, 1)
    ltv.T.plot(grid=True, ax=ax1)
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.ylabel('Стоимость, $')
    plt.title('LTV')

    # второй график — динамика ltv
    ax2 = plt.subplot(2, 3, 2, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in ltv_history.index.names if name not in ['dt']]
    filtered_data = ltv_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax2)
    plt.xlabel('Дата привлечения')
    plt.title('Динамика LTV пользователей на {}-й день'.format(horizon))

    # третий график — динамика cac
    ax3 = plt.subplot(2, 3, 3, sharey=ax1)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in cac_history.index.names if name not in ['dt']]
    filtered_data = cac_history.pivot_table(
        index='dt', columns=columns, values='cac', aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax3)
    plt.xlabel('Дата привлечения')
    plt.ylabel('Стоимость, $')
    plt.title('Динамика стоимости привлечения пользователей')

    # четвёртый график — кривые roi
    ax4 = plt.subplot(2, 3, 4)
    roi.T.plot(grid=True, ax=ax4)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.legend()
    plt.xlabel('Лайфтайм')
    plt.ylabel('Стоимость, $')
    plt.title('ROI')

    # пятый график — динамика roi
    ax5 = plt.subplot(2, 3, 5, sharey=ax4)
    # столбцами сводной таблицы станут все столбцы индекса, кроме даты
    columns = [name for name in roi_history.index.names if name not in ['dt']]
    filtered_data = roi_history.pivot_table(
        index='dt', columns=columns, values=horizon - 1, aggfunc='mean'
    )
    filter_data(filtered_data, window).plot(grid=True, ax=ax5)
    plt.axhline(y=1, color='red', linestyle='--', label='Уровень окупаемости')
    plt.xlabel('Дата привлечения')
    plt.ylabel('Стоимость, $')
    plt.title('Динамика ROI пользователей на {}-й день'.format(horizon))

    plt.tight_layout()
    plt.show()

Исследовательский анализ данных¶

К содержанию

  • Составьте профили пользователей. Определите минимальную и максимальную даты привлечения пользователей.
  • Выясните, из каких стран пользователи приходят в приложение и на какую страну приходится больше всего платящих пользователей. Постройте таблицу, отражающую количество пользователей и долю платящих из каждой страны.
  • Узнайте, какими устройствами пользуются клиенты и какие устройства предпочитают платящие пользователи. Постройте таблицу, отражающую количество пользователей и долю платящих для каждого устройства.
  • Изучите рекламные источники привлечения и определите каналы, из которых пришло больше всего платящих пользователей. Постройте таблицу, отражающую количество пользователей и долю платящих для каждого канала привлечения.

После каждого пункта сформулируйте выводы.

Создание профилей пользователей¶

К содержанию

У нас есть три таблицы, в которых записаны данные пользователей, данные посещений и покупок пользователей, а также траты на рекламу. Чтобы оценить прибыльность рекламных кампаний и от чего они зависят, необходимо составить профили пользователей. Для этого воспользуемся функцией get_profiles которую мы задали ранее, чтобы получить таблицу, в которой будет вся необходимая для анализа информация.

In [31]:
user_profiles = get_profiles(visits,orders,costs)
user_profiles.head()
Out[31]:
user_id first_ts channel device region dt month payer acquisition_cost
0 599326 2019-05-07 20:58:57 FaceBoom Mac United States 2019-05-07 2019-05-07 20:58:57 True 1.088172
1 4919697 2019-07-09 12:46:07 FaceBoom iPhone United States 2019-07-09 2019-07-09 12:46:07 False 1.107237
2 6085896 2019-10-01 09:58:33 organic iPhone France 2019-10-01 2019-10-01 09:58:33 False 0.000000
3 22593348 2019-08-22 21:35:48 AdNonSense PC Germany 2019-08-22 2019-08-22 21:35:48 False 0.988235
4 31989216 2019-10-02 00:07:44 YRabbit iPhone United States 2019-10-02 2019-10-02 00:07:44 False 0.230769

Итак, мы получили таблицу, в которой есть:

  • уникальный идентификатор пользователя user_id;
  • Дата и время первого посещения приложения first_ts;
  • Рекламный источник, который привлек данного пользователя channel;
  • Устройство, с которого пользователь зашел в приложение device;
  • Страна, из которой пришел пользователь region;
  • Дата первого посещения dt;
  • Первый день месяца, в которое произошло посещение month;
  • Совершил ли пользователь покупку payer;
  • Сколько стоило привлечение данного пользователя acquisition_cost.

Теперь мы сможем провести интересующий нас анализ окупаемости рекламы, но для начала поближе познакомимся с представленными данными.

In [32]:
#Получим граничные даты привлечения пользователей
min_acquisition_date = user_profiles['dt'].min()
max_acquisition_date = user_profiles['dt'].max()
print('Минимальная дата привлечения пользователей:', min_acquisition_date,
     '\nМаксимальная дата привлечения пользователей:', max_acquisition_date)
Минимальная дата привлечения пользователей: 2019-05-01 
Максимальная дата привлечения пользователей: 2019-10-27

Мы узнали, что пользователи привлекались в приложение с 1 мая по 27 октября 2019 года, как и было указано в описании данных. Теперь оценим, какие предпочтения у пользователей приложения в целом и что предпочитают платящие пользователи.

Предпочтения пользователей¶

К содержанию

Чтобы оценить из каких стран, с каких устройств и через какие рекламные источники приходят пользователи составим соответствующие таблицы. А также рассчитаем конверсию по каждому из этих признаков (какой процент пользователей стал "покупателем" в зависимости от изучаемого признака).

In [33]:
#Cтроим таблицу зависимости от страны пользователя
country_users = user_profiles.groupby('region').agg({'user_id': 'nunique', 
                                                     'payer': ['sum', 'mean']})
country_users.columns = ['user_id','payer_sum','payer_%']
country_users['payer_%'] = country_users['payer_%']*100
country_users = country_users.sort_values(by='payer_%', ascending = False)
In [34]:
#Таблица зависимости от устройства пользователя
device_users = user_profiles.groupby('device').agg({'user_id': 'nunique',
                                                    'payer': ['sum', 'mean']})
device_users.columns = ['user_id','payer_sum','payer_%']
device_users['payer_%'] = device_users['payer_%']*100
device_users = device_users.sort_values(by='payer_%', ascending = False)
In [35]:
#Таблица зависимости от рекламного источника
channel_users = user_profiles.groupby('channel').agg({'user_id': 'nunique', 
                                                      'payer': ['sum', 'mean']})
channel_users.columns = ['user_id','payer_sum','payer_%']
channel_users['payer_%'] = channel_users['payer_%']*100
channel_users = channel_users.sort_values(by='payer_%', ascending = False)
In [36]:
# Выведем все таблицы
display('Платящие пользователи по регионам', 
        country_users,
        'Платящие пользователи по устройствам',
        device_users,
        'Платящие пользователи по рекламным источникам',
        channel_users)
'Платящие пользователи по регионам'
user_id payer_sum payer_%
region
United States 100002 6902 6.901862
Germany 14981 616 4.111875
UK 17575 700 3.982930
France 17450 663 3.799427
'Платящие пользователи по устройствам'
user_id payer_sum payer_%
device
Mac 30042 1912 6.364423
iPhone 54479 3382 6.207897
Android 35032 2050 5.851793
PC 30455 1537 5.046790
'Платящие пользователи по рекламным источникам'
user_id payer_sum payer_%
channel
FaceBoom 29144 3557 12.204914
AdNonSense 3880 440 11.340206
lambdaMediaAds 2149 225 10.469986
TipTop 19561 1878 9.600736
RocketSuperAds 4448 352 7.913669
WahooNetBanner 8553 453 5.296387
YRabbit 4312 165 3.826531
MediaTornado 4364 156 3.574702
LeapBob 8553 262 3.063253
OppleCreativeMedia 8605 233 2.707728
organic 56439 1160 2.055316

Из полученных данных мы можем вынести следующие наблюдения:

  1. Больше всего пользователей приложения из США - около ста тысяч. Из других стран в приложение пришли не более 20 тысяч пользователей. И покупателями охотнее всего становятся пользователи из США - конверсия составляет около 7%, в Европейских странах покупателями становятся всего около 4% пользователей. Это может быть связано с тем, что в самом приложении есть какие-то региональные особенности, или же рекламные кампании проводятся с бОльшим упором на США, нежели Европу.

  2. Чаще всего приложением пользуются владельцы IPhone - 54 тысячи посетителей. На других устройствах примерно по 30 тысяч пользователей. Конверсия примерно одинакова для всех пользователей и составляет около 6%, кроме пользователей PC - там покупателями становятся только 5% пользователей. Значит приложение хорошо работает на всех платформах (возможно кроме PC) и окупаемость зависит от устройства не так сильно.

  3. Среди рекламных источников можно выделить топ-5 по конверсии:

    • FaceBoom
    • AdNonSense
    • lambdaMediaAds
    • TipTop
    • RocketSuperAds

Конверсия тут составляет более 6%. Больше всего пользователей среди этих источников привлекли FaceBoom и TipTop. Однако, наиболее успешными можно считать кампании рекламных источников AdNonSense и lambdaMediaAds, так как они привлекают покупающих клиентов - у них высокая конверсия при небольшом притоке пользователей (данные рекламные источники привлекают меньше всего пользователей (менее 4 тысяч), однако большинство этих пользователей становятся покупателями). Но больше всего пользователей пришли без учёта рекламных кампаний - 56 тысяч, но у них низкая вероятность стать покупателями.

Таким образом можно сделать следующий вывод: Компании следует провести анализ отличий рынка США и Европы и повысить заинтересованность европейских пользователей, а также пересмотреть затраты на рекламные кампании. Так как больше всего посетителей приходит без стороннего привлечения, стоит обратить внимание, возможно сама компания недостаточно прозрачно раскрыла потенциал покупки приложения и у рекламных агенств получилось лучше показать преимущества покупки.

In [37]:
#Визуализируем доли платящих пользователей в зависимости от признаков
plt.figure(figsize=(23,10))
plt.suptitle('Доля платящих пользователей', fontsize=20)
(user_profiles.groupby('region')['payer'].sum()
 .plot(kind='pie', y='region', autopct='%1.1f%%', ylabel=' ', ax=plt.subplot(1,3,1)))
plt.title('По странам', fontsize=16)
(user_profiles.groupby('device')['payer'].sum()
 .plot(kind='pie', y='device', autopct='%1.1f%%', ylabel=' ', ax=plt.subplot(1,3,2)))
plt.title('По устройствам', fontsize=16)
(user_profiles.groupby('channel')['payer'].sum()
 .plot(kind='pie', y='channel', autopct='%1.1f%%', ylabel=' ', ax=plt.subplot(1,3,3)))
plt.title('По рекламным каналам', fontsize=16);

Если оценивать по каждому источнику отдельно, что больше всего влияет на покупку приложения, можно сделать следующие выводы:

  1. Впринципе, как и говорилось раньше, больше всего платящих пользователей приходится на США - 77.7% от всех платящих пользователей, стоит узнать, что не устраивает пользователей из Европы и проанализировать различия рынка. А возможно причина в платежной системе - может приложение трудно оплатить не в долларах.
  2. Первое место занимает мобильная версия приложения для IPhone. Возможно это связано с тем, что в США очень распространены мобильные телефоны IPhone, либо мобильная версия более функциональна, чем десктопная. Стоит оценить различия для всех платформ и проанализировать, что больше всего нравится пользователям, которые предпочитают покупку приложения.
  3. Больше всего платящих пользователей среди тех, кто пришел благодаря рекламной кампании FaceBoom и TipTop, однако, на третьем месте по количеству покупателей - пользователи, которые пришли без использования рекламных источников. Стоит обратить на это внимание и изучить, какую стратегию используют успешные рекламные источники и что привлекает пользователей, пришедших самостоятельно. И сделать упор на раскрутку конкретно этих преимуществ.

Маркетинг¶

К содержанию

  • Посчитайте общую сумму расходов на маркетинг.
  • Выясните, как траты распределены по рекламным источникам, то есть сколько денег потратили на каждый источник.
  • Постройте визуализацию динамики изменения расходов во времени (по неделям и месяцам) по каждому источнику. Постарайтесь отразить это на одном графике.
  • Узнайте, сколько в среднем стоило привлечение одного пользователя (CAC) из каждого источника. Используйте профили пользователей.

Напишите промежуточные выводы.

Расходы на маркетинг¶

К содержанию

Оценим расходы на рекламу, сколько фирма всего потратила на рекламу и какие именно рекламные сервисы требуют больше всего денег.

In [38]:
print('Общая сумма расходов на маркетинг:', round(costs['costs'].sum(), 2))
Общая сумма расходов на маркетинг: 105497.3
In [39]:
costs.groupby('channel').agg({'costs':'sum'}).sort_values(by='costs',ascending=False)
Out[39]:
costs
channel
TipTop 54751.30
FaceBoom 32445.60
WahooNetBanner 5151.00
AdNonSense 3911.25
OppleCreativeMedia 2151.25
RocketSuperAds 1833.00
LeapBob 1797.60
lambdaMediaAds 1557.60
MediaTornado 954.48
YRabbit 944.22

Больше половины денег, затраченных на рекламу уходят источникам TipTop и FaceBoom, однако данные источники приносят не так много платящих пользователей. Как мы помним из предыдущего пункта, источники AdNonSence и lambdaMediaAds работают более "качественно" принося не так много пользователей компании как первые два источника, но тех, кто более охотно переходит в разряд покупателей. К тому же с меньшими тратами за всё исследуемое время. На первый взгляд стоит пересмотреть растраты, так как есть возможность при меньших затратах получать похожие результаты. Рассмотрим далее, так ли это.

Динамика расходов¶

К содержанию

Оценим динамику расходов, для этого добавим в таблицу с расходами дополнительные колонки.

In [40]:
#Меняем тип данных, чтобы создать новые колонки
costs['dt'] = pd.to_datetime(costs['dt'])
#Создаем новые колонки
costs['week'] = costs['dt'].dt.isocalendar().week
costs['month'] = costs['dt'].dt.month
costs.head()
Out[40]:
dt channel costs week month
0 2019-05-01 FaceBoom 113.3 18 5
1 2019-05-02 FaceBoom 78.1 18 5
2 2019-05-03 FaceBoom 85.8 18 5
3 2019-05-04 FaceBoom 136.4 18 5
4 2019-05-05 FaceBoom 122.1 18 5

Теперь построим графики, которые помогут оценить динамику изменения расходов по месяцам и по неделям.

In [41]:
plt.figure(figsize=(23,8))
plt.suptitle('Динамика изменения расходов во времени по рекламным источникам', fontsize=20)
(costs.pivot_table(index='week',columns='channel',values='costs',aggfunc='sum')
      .plot(grid=True, ylabel='Стоимость, $', xlabel='Неделя', ax=plt.subplot(1,2,1)));
plt.title('По неделям', fontsize=16);
(costs.pivot_table(index='month',columns='channel',values='costs',aggfunc='sum')
      .plot(grid=True, ylabel='Стоимость, $', xlabel='Месяц', ax=plt.subplot(1,2,2)));
plt.title('По месяцам', fontsize=16);

Мы можем отметить, что расходы на источники TipTop и FaceBoom имеют тенденцию к росту, фирма все больше и больше тратит деньги на два этих источника рекламы. При чем TipTop требует все больше расходов.

А что же с другими источниками? Уберем два наших "фаворита" по тратам и посмотрим, что же происходит с другими источниками.

In [42]:
plt.figure(figsize=(23,8))
plt.suptitle('Динамика изменения расходов во времени по рекламным источникам', fontsize=20)
(costs.query('channel != ["FaceBoom","TipTop"]')
      .pivot_table(index='week',columns='channel',values='costs',aggfunc='sum')
      .plot(grid=True, ylabel='Стоимость, $', xlabel='Неделя', ax=plt.subplot(1,2,1)));
plt.title('По неделям', fontsize=16);
(costs.query('channel != ["FaceBoom","TipTop"]')
      .pivot_table(index='month',columns='channel',values='costs',aggfunc='sum')
      .plot(grid=True, ylabel='Стоимость, $', xlabel='Месяц', ax=plt.subplot(1,2,2)));
plt.title('По месяцам', fontsize=16);

В данном случае, траты на остальные рекламные источники, кроме WahooNetBanner не меняются, или же медленно снижаются, особенно, если оценивать первый месяц/первые 4 недели наблюдений.

Скорее всего фирма оценивает эффективность рекламы по количеству привлеченных пользователей и вкладывается всё больше в те рекламные кампании, благодаря которым пришло много пользователей. Однако, для того, чтобы эта реклама окупилась, стоит оценивать конверсию пользователей в покупателей и отдавать предпочтение тем фирмам, которые привлекают именно покупателей.

Стоимость привлечения одного пользователя¶

К содержанию

Оценим, как распределилась стоимость привлечения пользователей для каждой рекламной кампании

In [43]:
#Удалим канал привлечения organic, потому что на него не идут расходы
cac = (user_profiles.query('channel !="organic"')
       .groupby('channel')
       .agg({'acquisition_cost':'mean'}).reset_index()
       .rename(columns={'acquisition_cost':'cac'})
       .sort_values(by='cac',ascending=False)
      )
cac
Out[43]:
channel cac
6 TipTop 2.799003
1 FaceBoom 1.113286
0 AdNonSense 1.008054
9 lambdaMediaAds 0.724802
7 WahooNetBanner 0.602245
5 RocketSuperAds 0.412095
4 OppleCreativeMedia 0.250000
8 YRabbit 0.218975
3 MediaTornado 0.218717
2 LeapBob 0.210172
In [44]:
avg_cac = user_profiles.query('channel != "organic"')
avg_cac = avg_cac['acquisition_cost'].mean()
print('Средняя стоимость привлечения одного клиента составляет {} долларов'.format(round(avg_cac,2)))
Средняя стоимость привлечения одного клиента составляет 1.13 долларов

Дороже всего стоит привлечение пользователей через источник TipTop- почти 3 доллара за человека, что в два раза превышает среднюю стоимость привлечения одного клиента. При этом, у остальных рекламных источников стоимость составляет примерно один доллар. Также, стоит отметить, что TipTop не самый эффективный источник, он привлекает не самое большое число пользователей и находится на 5 месте по конверсии в покупателей (примерно 9.7%). Кажется фирме стоит пересмотреть выбор рекламных источников. Но для начала проверим окупаемость, не будем "рубить с плеча".

Оценка окупаемости рекламы¶

К содержанию

Используя графики LTV, ROI и CAC, проанализируйте окупаемость рекламы. Считайте, что на календаре 1 ноября 2019 года, а в бизнес-плане заложено, что пользователи должны окупаться не позднее чем через две недели после привлечения. Необходимость включения в анализ органических пользователей определите самостоятельно.

  • Проанализируйте окупаемость рекламы c помощью графиков LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проверьте конверсию пользователей и динамику её изменения. То же самое сделайте с удержанием пользователей. Постройте и изучите графики конверсии и удержания.
  • Проанализируйте окупаемость рекламы с разбивкой по устройствам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проанализируйте окупаемость рекламы с разбивкой по странам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Проанализируйте окупаемость рекламы с разбивкой по рекламным каналам. Постройте графики LTV и ROI, а также графики динамики LTV, CAC и ROI.
  • Ответьте на такие вопросы:
    • Окупается ли реклама, направленная на привлечение пользователей в целом?
    • Какие устройства, страны и рекламные каналы могут оказывать негативное влияние на окупаемость рекламы?
    • Чем могут быть вызваны проблемы окупаемости?

Напишите вывод, опишите возможные причины обнаруженных проблем и промежуточные рекомендации для рекламного отдела.

Подготовка данных¶

К содержанию

Перейдём к оценке окупаемости рекламы. Для этого отфильтруем наши данные:

  • Зададим момент анализа - 1.11.2019
  • Горизонт анализа - 14 дней
  • Удалим из выборки пользователей, которые пришли вне рекламных кампаний (на их привлечение не идут траты)
In [45]:
profiles_filtred = user_profiles.query('channel != "organic"')
observation_date = datetime(2019,11,1).date()
horizon_days = 14

Окупаемость рекламы¶

К содержанию

Оценим окупаемость рекламы при помощи метрик:

  • LTV - сколько пользователь приносит денег компании
  • CAC - стоимость привлечения одного пользователя
  • ROI - окупаемость рекламы (насколько больше денег пользователь приносит, чем требует на привлечение) Для этого воспользуемся заданной ранее функцией get_ltv.
In [46]:
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(profiles_filtred, orders, observation_date, horizon_days)

plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)

LTV растёт, как и положено, однако, можно отметить, что САС растёт быстрее, и таким образом, рекламные затраты совершенно не окупаются. В динамике LTV снижается, как и ROI, все указывает на то, что пользователи не окупаются через 2 недели. Кажется фирме стоит пересмотреть свои рекламные расходы.

Конверсия покупателей и её динамика¶

К содержанию

Посмотрим, как охотно пользователи приложения становятся покупателями.

In [47]:
conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles_filtred, orders, observation_date, horizon_days)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Конверсия растёт и находится в нормальных пределах. С тем, чтобы пользователи становились покупателями проблем не отмечается.

Удержание пользователей¶

К содержанию

Оценим, как быстро падает заинтересованность пользователей после привлечения

In [48]:
retention_raw, retention_grouped, retention_history = get_retention(
    profiles_filtred, visits, observation_date, horizon_days
)

plot_retention(retention_grouped, retention_history, horizon_days) 

Удержание пользователей снижается, неплатящие пользователи "уходят" быстрее и скорее всего не продержатся две недели. Платящие пользователи держатся гораздо стабильнее, что не удивительно, есть дополнительный "стимул". Но всё равно, нельзя сказать, что пользователи плохо удерживаются, всё выглядит нормально и даже неплохо.

Окупаемость рекламы с разбивкой по устройствам¶

К содержанию

А теперь перейдём к тому, как именно окупается реклама и что на это влияет, начнём с устройств

In [49]:
dimensions = ['device']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions
)
plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

Больше всего денег фирме приносят пользователи Mac,iPhone, Android, меньше всего - PC. Стоимость привлечения также растёт к 14 дню. Однако, из всех пользователей по прошествию второй недели окупаются только пользователи PC и то с трудом. Но на их привлечение и тратят меньше всего. Кажется стоит снизить затраты на привлечение пользователей с конкретными устройствами. Похоже, отдел маркетинга основывается на предположении, что пользователи продукции Apple охотнее тратят деньги. Но судя по графикам - ошибочно.

Конверсия пользователей в зависимости от устройства¶
In [50]:
dimensions = ['device']

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Конверсия в зависимости от устройств стабильная, охотнее всего платящими пользователями становятся пользователи Mac и iPhone. Хуже всего конвертируются пользователи PC. Также, конверсия пользователей РС больше подвергается сезонности. Скорее всего на девайсах фирмы Apple более удобная система оплаты, да и эти пользователи считаются более "платежеспособными".

Удержание пользователей в зависимости от устройства¶
In [51]:
dimensions = ['device']
retention_raw, retention_grouped, retention_history = get_retention(
    profiles_filtred, visits, observation_date, horizon_days, dimensions=dimensions)

plot_retention(retention_grouped, retention_history, horizon_days) 

Удержание же показывает картину, обратную конверсии - пользователи РС гораздо лучше удерживаются, однако и удержание носит для этих пользователей сезонный характер. А вот пользователи девайсов Apple удерживаются хуже. Возможно пользователи РС более избирательны и делают более обдуманный выбор в пользу покупки.

Окупаемость рекламы с разбивкой на регионы¶

К содержанию

Как обстоят дела с окупаемостью рекламы в разных странах

In [52]:
dimensions = ['region']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 

Больше всего денег фирме приносят пользователи из США, что не удивительно, ведь больше всего пользователей именно из США. Однако, если оценить внимательнее, скорее всего это связано с тем, что огромные суммы денег тратятся именно на привлечение пользователей из США. Причину этого трудно выявить. Несмотря на все старания и на такое большое количество пользователей, реклама на привлечение в США не окупается. Эту стратегию также придётся пересмотреть.

Конверсия пользователей в зависимости от региона¶
In [53]:
dimensions = ['region']

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Как и ожидалось, пользователи из США активнее становятся покупателями, они менее подверженны сезонности, чем пользователи из Европы. Видимо рекламные кампании невероятно эффективны в США, или же приложение больше ориентировано на пользователей именно этого региона (может обновления выходят в неудобное для европейских пользователей время).

Удержание пользователей в зависимости от региона¶
In [54]:
dimensions = ['region']
retention_raw, retention_grouped, retention_history = get_retention(
    profiles_filtred, visits, observation_date, horizon_days, dimensions=dimensions)

plot_retention(retention_grouped, retention_history, horizon_days) 

А вот согласно данным удержания, пользователи США уходят также легко как и приходят. Среди Европейских пользователей привлекается больше "качественных" пользователей. Однако, удержание платящих пользователей США менее подверженно сезонности, чем в странах Европы.

Окупаемость рекламы с разбивкой на каналы привлечения¶

К содержанию

А теперь переходим к самому важному и наиболее интересному пункту. Ранее мы выяснили, что на некоторые рекламные источники уходит слишком много денег. Теперь настала пора проверить, насколько же это оправданно.

In [55]:
dimensions = ['channel']

ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions
)

plot_ltv_roi(
    ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days, window=14
) 
  • Больше всего денег компании приносят пользователи, пришедшие по каналам lambdaMediaAds и TipTop
  • Стоимость привлечения одного пользователя через TipTop невероятно высокая и растет с каждым месяцем почти в геометрической прогрессии. При этом, данный канал привлечения совершенно не окупается.
  • FaceBoom и AdNonSense также совершенно не окупаются.

Кажется мы нашли основную причину убытков компании - все деньги уходят на рекламные кампании, которые не приносят никакой выгоды. Кажется отделу маркетинга стоит пересмотреть рекламные стратегии и обратить внимание на конверсию пользователей, которую мы рассчитали ранее.

Конверсия пользователей в зависимости от канала привлечения¶
In [56]:
dimensions = ['channel']

conversion_raw, conversion_grouped, conversion_history = get_conversion(
    profiles_filtred, orders, observation_date, horizon_days, dimensions=dimensions)

plot_conversion(conversion_grouped, conversion_history, horizon_days)

Наибольшую конверсию приносят каналы FaceBoom, AdNonSense, lambdaMediaAds, TipTop, из них, менее всего подвержен сезонности канал FaceBoom. Хуже всего конвертируются пользователи каналов OppleCreativeMedia, LeapBob.

Удержание пользователей в зависимости от канала привлечения¶
In [57]:
dimensions = ['channel']
retention_raw, retention_grouped, retention_history = get_retention(
    profiles_filtred, visits, observation_date, horizon_days, dimensions=dimensions)

plot_retention(retention_grouped, retention_history, horizon_days) 

Картина удержания поражает. Два самых успешных канала привлечения AdNonSense и FaceBoom приводят пользователей, которые навряд ли продержатся две недели. А вот удержание пользователей от канала TipTop показывает хорошие результаты. Однако это все равно мало оправдывает такие высокие затраты.

Успешность рекламных акций в разных странах¶

Проведем анализ, чтобы узнать какие рекламные агенства успешны в США и в Европе. Так можно будет дать корректные рекомендации и лучше оценить закономерности.

In [58]:
usa_users = profiles_filtred.query('region == "United States"')
eu_users = profiles_filtred.query('region != "United States"')
Для США¶
In [59]:
#Оценим окупаемость
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    usa_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
In [60]:
#Оценим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
    usa_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_conversion(conversion_grouped, conversion_history, horizon_days)
In [61]:
#Оценим удержание
retention_raw, retention_grouped, retention_history = get_retention(
    usa_users, visits, observation_date, horizon_days, dimensions=['channel'])
plot_retention(retention_grouped, retention_history, horizon_days)

В США в лидеры выбиваются рекламные фирмы YRabbit, MediaTornado, RocketSuperAds, они достаточно дешевые, у них хороший уровень удержания и они отлично себя окупают уже к третьему дню. Самый лучший рекламный источник здесь - RocketSuperAds, так как у него отличный уровень конверсии. Если снизить расходы на источник TipTop, возможно он начнет себя окупать.

Для Европы¶
In [62]:
#Оценим окупаемость
ltv_raw, ltv_grouped, ltv_history, roi_grouped, roi_history = get_ltv(
    eu_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_ltv_roi(ltv_grouped, ltv_history, roi_grouped, roi_history, horizon_days)
In [63]:
#Оценим конверсию
conversion_raw, conversion_grouped, conversion_history = get_conversion(
    eu_users, orders, observation_date, horizon_days, dimensions = ['channel'])
plot_conversion(conversion_grouped, conversion_history, horizon_days)
In [64]:
#Оценим удержание
retention_raw, retention_grouped, retention_history = get_retention(
    eu_users, visits, observation_date, horizon_days, dimensions=['channel'])
plot_retention(retention_grouped, retention_history, horizon_days)

В Европе ситуация обстоит лучше, единственная фирма, которая не окупается - AdNonSense, она же не может удержать пользователей. В среднем здесь реклама окупается на 5 день привлечения. Фаворитом является фирма lambdaMediaAds - клиенты от этой фирмы приносят хороший доход, кампания окупается, хорошая конверсия пользователей и нормальное удержание. И она дешевле, чем AdNonSense. Кажется Европейскому отделу маркетинга стоит пересмотреть вложения средств.

Вывод¶

Из проведенного анализа можно сделать вывод, что реклама не окупается.

  • Со временем стоимость рекламы только растёт, а интерес пользователей к совершению покупки не успевает за ростом стоимости привлечения.
  • Привлечение целевых пользователей из США осталвяет желать лучшего, ведь эти пользователи совершенно не окупаются и быстро теряют интерес к приложению.
  • Высоки также затраты на привлечение пользователей устройств фирмы Apple - при нормальной конверсии и хорошем удержании реклама совершенно не окупается.
  • Имеются проблемы с рекламой источников FaceBoom, TipTop и AdNonSense - это самые дорогостоящие источники, которые имеют хорошую конверсию, однако у них есть ряд проблем - FaceBoom и AdNonSence приносят компании пользователей, которые не удерживаются в течение двух недель, а TipTop требует огромного количества расходов.

Можно выделить ряд закономерностей, которые могут помочь отделу маркетинга справиться с возникшим кризисом

Напишите выводы¶

К содержанию

  • Выделите причины неэффективности привлечения пользователей.
  • Сформулируйте рекомендации для отдела маркетинга.

Основная причина неэффективности привлечения пользователей кроется в том, что выбрана неверная стратегия раскрутки приложения. Скорее всего, проблема состоит в том, что компания стремится вкладываться в то, что по их мнению "работает". Видимо оценка эффективности для отдела маркетинга в данном случае складывается из количества привлеченных пользователей. Возможно, они хотят взять количеством пользователей, а не "качеством".

На основании полученных результатов можно выделить следующие рекомендации:

  1. Следует снизить расходы на рекламу:
    • Снизить расходы на источник TipTop, конверсия и удержание пользователей от этого канала в норме, однако траты на этот канал неустанно растут, при условии, что это не самый "успешный" канал привлечения. А также это самый дорогостоящий канал, который составляет более половины трат на рекламу (54 тысячи долларов из 105 тысяч в общем).
    • Урезать расходы на рекламу в США - да, это целевая аудитороия приложения, но эти расходы совершенно не окупаются, в отличие от привлечения пользователей из стран Европы. По распределению трат на привлечение по странам в США самая высокая стоимость привлечения, и также высокая конверсия, но удержание этих пользователей оставляет желать лучшего. Стоит потратить эти деньги на что-то другое. А также попробовать обратить внимание на фирму RocketSuperAds, так как она, пусть и уступает фирме TipTop, но имеет хорошие показатели в США.
    • В странах Европы следует сделать акцент на рекламной фирме lambdaMediaAds, так как она показывает отличные результаты и требует меньше вложений.
    • Пересмотреть рекламные кампании фирм FaceBoom и AdNonSense - эти фирмы также в топе по стоимости проведения акций, и приносят хорошую конверсию, однако пользователи от этих фирм совершенно не удерживаются. Возможно дело в самих акциях - может они слишком приукрашены и реклама не соответсвтует действительности, либо дело в том, какой формат оплаты рекламы у этих фирм, стоит пересмотреть эту специфику. Скорее всего такой тип не подходит и слишком дорогостоящий, по сравнению с фирмами-конкурентами.
    • Стоимость рекламы для пользователей устройств Apple также не выглядит целесообразной. Здесь не наблюдается никаких проблем с привлечением и удержанием, но такой подход не окупается. Кажется стоит обратить внимание на то, как и каким образом проводятся акции для привлечения пользователей РС. Да, это может несколько снизить приток пользователей, но в настоящее время следует сделать упор на решение возникшего кризиса.
  2. Сделать упор на конверсию и удержание:
    • Предложить акции или скидки, пользователям, которые пришли в приложение "органическим" путём - этих пользователей приходит гораздо больше, чем после проведения рекламных кампаний. Если снизить расходы, как предлагается в первом пункте, можно будет попробовать вложиться в акции и скидки.
    • Ввести систему поощрений для пользователей, совершивших покупку. Проводить ежемесячные эвенты, чтобы снизить сезонный отток пользователей.
  3. Увеличить охват пользователей:
    • Проведение акций в других странах (вне США).
    • Проанализировать успешность приложения на устройствах фирмы Apple и предложить остальным пользователям ту же функициональность (может дело в интерфейсе, может добавить для других пользователей более удобные методы оплаты).
    • Сезонные скидки, скидки для определенных категорий пользователей.
  4. Скооперироваться с техническим отделом/разработчиками:
    • Провести исследование удобства использования приложения на разных платформах.
    • Узнать, есть ли какие-то проблемы с приложением, которые отталкивают пользователей от покупки:
      • Проблемы с оплатой в других странах,
      • Не работает какая-то часть интерфейса.

Последний пункт не является обязательным, так как конверсия пользователей идет хорошо и скорее всего в технической части приложения нет никаких проблем.

In [ ]: